%%	Provide encode/decode message for m3ua
%%	@author : Tran Hoan - tvhoan88@gmail.com

-module(ht_m3ua_codec).

-include("ht_m3ua_message.hrl").

-export([encode/1,
		 decode/1,
		 test/0
		]).

-export([encode_param/3,
		 encode_param/4
		]).

decode(<<Ver,Reserved,Class,Type,Len:32,Msg/binary>>) when Len == (size(Msg) + 8) ->
	Body = decode_m3ua_msg(Class,Type,Msg),
	#ht_m3ua_msg{version = Ver,
				 reserved = Reserved,
				 msg_class = Class,
				 msg_type = Type,
				 msg_body = Body};

decode(_) ->
	erlang:error(malform_m3ua_message).

encode(#ht_m3ua_msg{version = Ver,reserved = Reserved,
					msg_class = Class,msg_type = Type,
					msg_body = Body}) ->
	BodyBin = encode_m3ua_msg(Class,Type,Body),
	Size = byte_size(BodyBin) + 8,
	{Len,Bin} =	case Size band 3 of 
					0 -> 
						{Size,BodyBin};
					R ->
						Ar = 4 - R,
						Arb = Ar bsl 3,
						{Size + Ar,<<BodyBin/binary,0:Arb>>}
				end,
	<<Ver,Reserved,Class,Type,Len:32,Bin/binary>>;

encode(_) ->
	erlang:error(invalid_m3ua_msg).

encode_m3ua_msg(?ht_m3ua_transfer,?ht_m3ua_data,
				#ht_m3ua_transfer_data{network_appearance = Na,
									  correlation_id = CorId,
									  routing_context = Rc,
									  protocol_data = Proto}) ->
	Params = [{?ht_m3ua_network_appearance_tag,Na,false},
			{?ht_m3ua_routing_context_tag,Rc,false},
			{?ht_m3ua_protocol_data_tag,Proto,true},
			{?ht_m3ua_correlation_id_tag,CorId,false}],
	lists:foldl(fun({T,V,M},Buf) ->
						encode_param(Buf,T,V,M)
				end,<<>>,Params);
%% ASP SM
encode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspup,
				#ht_m3ua_aspsm_aspup{asp_id = AspID,
									 info_string = Info
									 }) ->
	Params = [{?ht_m3ua_asp_id_tag,AspID,false},
			{?ht_m3ua_info_string_tag,Info,false}],
	lists:foldl(fun({T,V,M},Buf) ->
						encode_param(Buf,T,V,M)
				end,<<>>,Params);

encode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspup_ack,
				#ht_m3ua_aspsm_aspup_ack{asp_id = AspID,
									 info_string = Info
									 }) ->
	Params = [{?ht_m3ua_asp_id_tag,AspID,false},
			{?ht_m3ua_info_string_tag,Info,false}],
	lists:foldl(fun({T,V,M},Buf) ->
						encode_param(Buf,T,V,M)
				end,<<>>,Params);

encode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspdn,
				#ht_m3ua_aspsm_aspdown{info_string = Info}) ->
	Params = [{?ht_m3ua_info_string_tag,Info,false}],
	lists:foldl(fun({T,V,M},Buf) ->
						encode_param(Buf,T,V,M)
				end,<<>>,Params);

encode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspdn_ack,
				#ht_m3ua_aspsm_aspdown_ack{info_string = Info}) ->
	Params = [{?ht_m3ua_info_string_tag,Info,false}],
	lists:foldl(fun({T,V,M},Buf) ->
						encode_param(Buf,T,V,M)
				end,<<>>,Params);

%% ASP TM
encode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspac,
				#ht_m3ua_asptm_aspac{traffic_mode = Mode,
									 routing_context = RC,
									 info_string = Info}) ->
	Params = [{?ht_m3ua_traffic_mode_tag,Mode,false},
			  {?ht_m3ua_routing_context_tag,RC,false},
			  {?ht_m3ua_info_string_tag,Info,false}],
	lists:foldl(fun({T,V,M},Buf) ->
						encode_param(Buf,T,V,M)
				end,<<>>,Params);

encode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspac_ack,
				#ht_m3ua_asptm_aspac_ack{traffic_mode = Mode,
									 routing_context = RC,
									 info_string = Info}) ->
	Params = [{?ht_m3ua_traffic_mode_tag,Mode,false},
			  {?ht_m3ua_routing_context_tag,RC,false},
			  {?ht_m3ua_info_string_tag,Info,false}],
	lists:foldl(fun({T,V,M},Buf) ->
						encode_param(Buf,T,V,M)
				end,<<>>,Params);

encode_m3ua_msg(_,_,_) ->
	erlang:error(invalid_m3ua_msg).

%% internal function
-spec encode_param(Buf :: binary(), Tag :: non_neg_integer(),Value :: any()) ->
	Ret :: binary().
encode_param(Buf,Tag,Value) ->
	encode_param(Buf,Tag,Value,false).

-spec encode_param(Buf :: binary(),Tag :: non_neg_integer(),
					Value :: any(), Mandatory :: boolean()) 
		-> Ret :: binary().

%% Protocol Data
encode_param(Bin,?ht_m3ua_protocol_data_tag,
			 #ht_m3ua_proto_data{opc = Opc,dpc = Dpc,
								 si = SI,ni = Ni,mp = Mp,
								 sls = Sls,data = Data},
			 _) ->
	DataBin = <<Opc:32,Dpc:32,SI,Ni,Mp,Sls,Data/binary>>,
	Len = size(DataBin) + 4,
	<<Bin/binary,?ht_m3ua_protocol_data_tag:16,Len:16,DataBin/binary>>;

%% Network Apperance
encode_param(Bin,?ht_m3ua_network_appearance_tag,V,_) when is_integer(V), V > 0 ->
	<<Bin/binary,?ht_m3ua_network_appearance_tag:16,8:16,V:32>>;

%% Routing Context
encode_param(Bin,?ht_m3ua_routing_context_tag,V,_) when is_integer(V), V > 0 ->
	<<Bin/binary,?ht_m3ua_routing_context_tag:16,8:16,V:32>>;

encode_param(Bin,?ht_m3ua_routing_context_tag,V,_) when is_list(V) ->
	VBin = lists:foldl(fun(T,Acc) ->
							   <<Acc/binary,T:32>>
					   end,<<>>,V),
	Len = byte_size(VBin) + 4,
	<<Bin/binary,?ht_m3ua_routing_context_tag:16,Len:16,VBin/binary>>;

%% Correlation ID
encode_param(Bin,?ht_m3ua_correlation_id_tag,V,_) when is_integer(V), V > 0 ->
	<<Bin/binary,?ht_m3ua_correlation_id_tag:16,8:16,V:32>>;

%% ASP ID
encode_param(Bin,?ht_m3ua_asp_id_tag,V,_) when is_integer(V), V > 0 ->
	<<Bin/binary,?ht_m3ua_asp_id_tag:16,8:16,V:32>>;

%% INFO string
encode_param(Bin,?ht_m3ua_asp_id_tag,V,_) when is_list(V), V > 0 ->
	VBin = iolist_to_binary(V),
	Len = byte_size(VBin) + 4,
	<<Bin/binary,?ht_m3ua_asp_id_tag:16,Len:16,VBin/binary>>;

%% Optional Tag
encode_param(Bin,Tag,_,false) when is_integer(Tag) ->
	Bin;


encode_param(_,_,_,_) ->
	erlang:error(invalid_m3ua_param).

%% DATA message
decode_m3ua_msg(?ht_m3ua_transfer,?ht_m3ua_data,Bin) ->
	Params = [{?ht_m3ua_network_appearance_tag,optional},
			  {?ht_m3ua_routing_context_tag,conditional},
			  {?ht_m3ua_protocol_data_tag,mandatory},
			  {?ht_m3ua_correlation_id_tag,optional}
			 ],
	case catch lists:foldl(fun(T,{List,Data}) ->
							  {Param,Data1} = decode_param(T,Data),
							  {[Param | List],Data1}
					  end,{[ht_m3ua_transfer_data],Bin},Params) of
		{L,<<>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0,0>>} ->
			list_to_tuple(lists:reverse(L));
		Exp ->
			error_logger:error_msg("decode_m3ua_msg: get exception ~p~n",[Exp]),
			erlang:error(malform_m3ua_message)
	end;

%% ASP SM message

decode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspup,Bin) ->
	Params = [{?ht_m3ua_asp_id_tag,optional},
			  {?ht_m3ua_info_string_tag,optional}
			 ],
	case catch lists:foldl(fun(T,{List,Data}) ->
							  {Param,Data1} = decode_param(T,Data),
							  {[Param | List],Data1}
					  end,{[ht_m3ua_aspsm_aspup],Bin},Params) of
		{L,<<>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0,0>>} ->
			list_to_tuple(lists:reverse(L));
		Exp ->
			error_logger:error_msg("decode_m3ua_msg: get exception ~p~n",[Exp]),
			erlang:error(malform_m3ua_message)
	end;

decode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspup_ack,Bin) ->
	Params = [{?ht_m3ua_asp_id_tag,optional},
			  {?ht_m3ua_info_string_tag,optional}
			 ],
	case catch lists:foldl(fun(T,{List,Data}) ->
							  {Param,Data1} = decode_param(T,Data),
							  {[Param | List],Data1}
					  end,{[ht_m3ua_aspsm_aspup_ack],Bin},Params) of
		{L,<<>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0,0>>} ->
			list_to_tuple(lists:reverse(L));
		Exp ->
			error_logger:error_msg("decode_m3ua_msg: get exception ~p~n",[Exp]),
			erlang:error(malform_m3ua_message)
	end;

decode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspdn,Bin) ->
	Params = [{?ht_m3ua_info_string_tag,optional}
			 ],
	case catch lists:foldl(fun(T,{List,Data}) ->
							  {Param,Data1} = decode_param(T,Data),
							  {[Param | List],Data1}
					  end,{[ht_m3ua_aspsm_aspdown],Bin},Params) of
		{L,<<>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0,0>>} ->
			list_to_tuple(lists:reverse(L));
		Exp ->
			error_logger:error_msg("decode_m3ua_msg: get exception ~p~n",[Exp]),
			erlang:error(malform_m3ua_message)
	end;

decode_m3ua_msg(?ht_m3ua_aspsm,?ht_m3ua_aspdn_ack,Bin) ->
	Params = [{?ht_m3ua_info_string_tag,optional}
			 ],
	case catch lists:foldl(fun(T,{List,Data}) ->
							  {Param,Data1} = decode_param(T,Data),
							  {[Param | List],Data1}
					  end,{[ht_m3ua_aspsm_aspdown_ack],Bin},Params) of
		{L,<<>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0>>} ->
			list_to_tuple(lists:reverse(L));
		{L,<<0,0,0>>} ->
			list_to_tuple(lists:reverse(L));
		Exp ->
			error_logger:error_msg("decode_m3ua_msg: get exception ~p~n",[Exp]),
			erlang:error(malform_m3ua_message)
	end;

decode_m3ua_msg(_,_,_) ->
	erlang:error(malform_m3ua_message).

decode_param({mandatory,_},<<>>) ->
	erlang:error(malform_m3ua_message);

decode_param({_,_},<<>>) ->
	{undefined,<<>>};

decode_param({?ht_m3ua_routing_context_tag,Spec},<<8:16,_/binary>> = Bin) ->
	decode_unsigned_int({?ht_m3ua_routing_context_tag,Spec},Bin);

decode_param({?ht_m3ua_routing_context_tag,Spec},Bin) ->
	decode_variable_len({?ht_m3ua_routing_context_tag,Spec},Bin);

decode_param({Tag,Spec},Bin) 
  when Tag == ?ht_m3ua_network_appearance_tag;
	   Tag == ?ht_m3ua_correlation_id_tag;
	   Tag == ?ht_m3ua_traffic_mode_tag;
	   Tag == ?ht_m3ua_asp_id_tag ->
	decode_unsigned_int({Tag,Spec}, Bin);

decode_param({Tag,Spec},Bin) 
  when Tag == ?ht_m3ua_protocol_data_tag;
	   Tag == ?ht_m3ua_info_string_tag;
	   Tag == ?ht_m3ua_routing_key_tag ->
	decode_variable_len({Tag,Spec}, Bin);

decode_param(_,_) ->
	erlang:error(unrecognize_parameter).

decode_variable_len({Tag,mandatory},<<Tag:16,Len:16,Ret/binary>>) ->
	VLen = Len - 4,
	case Ret of
		<<Val:VLen/binary>> ->
			{decode_param_detail(Tag,Val),<<>>};
		<<Val:VLen/binary,Rem/binary>> ->
			{decode_param_detail(Tag,Val),Rem};
		_ ->
			erlang:error(invalid_m3ua_param)
	end;

decode_variable_len({Tag,_},<<Tag:16,Len:16,Ret/binary>>) ->
	VLen = Len - 4,
	case Ret of
		<<Val:VLen/binary>> ->
			{decode_param_detail(Tag,Val),<<>>};
		<<Val:VLen/binary,Rem/binary>> ->
			{decode_param_detail(Tag,Val),Rem};
		_ ->
			erlang:error(invalid_m3ua_param)
	end;

decode_variable_len({_,mandatory},_) ->
	erlang:error(invalid_m3ua_param);

decode_variable_len(_,Bin) ->
	{undefined,Bin}.

decode_unsigned_int({Tag,_},<<Tag:16,8:16,Val:32>>) ->
	{Val,<<>>};

decode_unsigned_int({Tag,_},<<Tag:16,8:16,Val:32,Ret/binary>>) ->
	{Val,Ret};

decode_unsigned_int({_,mandatory},_) ->
	erlang:error(invalid_m3ua_param);

decode_unsigned_int(_,Bin) ->
	{undefined,Bin}.

%% Protocol Data
decode_param_detail(?ht_m3ua_protocol_data_tag,<<Opc:32,Dpc:32,SI,Ni,Mp,Sls,Data/binary>>) ->
	#ht_m3ua_proto_data{opc = Opc,dpc = Dpc,
					 si = SI,ni = Ni,mp = Mp,
					 sls = Sls,data = Data};

%% Info String
decode_param_detail(?ht_m3ua_info_string_tag,Val) ->
	binary_to_list(Val);

%% Routing Context
decode_param_detail(?ht_m3ua_routing_context_tag,Val) ->
	rc2list(Val,[]);

decode_param_detail(_,_) ->
	erlang:error(invalid_m3ua_tag).

rc2list(<<>>,L) ->
	lists:reverse(L);

rc2list(<<Rc:32,Ret/binary>>,L) ->
	rc2list(Ret,[Rc|L]);

rc2list(Rc,L) ->
	error_logger:warning_msg("rc2list: invalid routing context key ~p~n",[Rc]),
	rc2list(<<>>,L).

%% unit test
test() ->
	M3UATransfer = hex_dump:parse_hex_str("01000101000000ac000600080000000202100099000005d40000052a0303000f0981030e190b12920011044889200054030b12080011044889200096026b626948040007fff06b1a2818060700118605010101a00d600ba1090607040000010032016c45a143020100020100303b80010182070390612310932983070393612772670485010abb0580038090a39c010c9f320854028408623500f1bf35038301119f3605a323400004000000"),
	A = decode(M3UATransfer),
	B = encode(A),
	hex_dump:compare(B,M3UATransfer).